/*
 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package com.sun.java.swing.plaf.gtk;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.colorchooser.*;
import javax.swing.event.*;
import javax.swing.plaf.*;

/**
 * A color chooser panel mimicking that of GTK's: a color wheel showing
 * hue and a triangle that varies saturation and brightness.
 *
 * @author Scott Violet
 */
class GTKColorChooserPanel extends AbstractColorChooserPanel implements
    ChangeListener {

  private static final float PI_3 = (float) (Math.PI / 3);

  private ColorTriangle triangle;
  private JLabel lastLabel;
  private JLabel label;

  private JSpinner hueSpinner;
  private JSpinner saturationSpinner;
  private JSpinner valueSpinner;

  private JSpinner redSpinner;
  private JSpinner greenSpinner;
  private JSpinner blueSpinner;

  private JTextField colorNameTF;

  private boolean settingColor;

  // The colors are mirrored to avoid creep in adjusting an individual
  // value.
  private float hue;
  private float saturation;
  private float brightness;


  /**
   * Convenience method to transfer focus to the next child of component.
   */
  // PENDING: remove this when a variant of this is added to awt.
  static void compositeRequestFocus(Component component, boolean direction) {
    if (component instanceof Container) {
      Container container = (Container) component;
      if (container.isFocusCycleRoot()) {
        FocusTraversalPolicy policy = container.
            getFocusTraversalPolicy();
        Component comp = policy.getDefaultComponent(container);
        if (comp != null) {
          comp.requestFocus();
          return;
        }
      }
      Container rootAncestor = container.getFocusCycleRootAncestor();
      if (rootAncestor != null) {
        FocusTraversalPolicy policy = rootAncestor.
            getFocusTraversalPolicy();
        Component comp;

        if (direction) {
          comp = policy.getComponentAfter(rootAncestor, container);
        } else {
          comp = policy.getComponentBefore(rootAncestor, container);
        }
        if (comp != null) {
          comp.requestFocus();
          return;
        }
      }
    }
    component.requestFocus();
  }


  /**
   * Returns a user presentable description of this GTKColorChooserPane.
   */
  public String getDisplayName() {
    return (String) UIManager.get("GTKColorChooserPanel.nameText");
  }

  /**
   * Returns the mnemonic to use with <code>getDisplayName</code>.
   */
  public int getMnemonic() {
    String m = (String) UIManager.get("GTKColorChooserPanel.mnemonic");

    if (m != null) {
      try {
        int value = Integer.parseInt(m);

        return value;
      } catch (NumberFormatException nfe) {
      }
    }
    return -1;
  }

  /**
   * Character to underline that represents the mnemonic.
   */
  public int getDisplayedMnemonicIndex() {
    String m = (String) UIManager.get(
        "GTKColorChooserPanel.displayedMnemonicIndex");

    if (m != null) {
      try {
        int value = Integer.parseInt(m);

        return value;
      } catch (NumberFormatException nfe) {
      }
    }
    return -1;
  }

  public Icon getSmallDisplayIcon() {
    return null;
  }

  public Icon getLargeDisplayIcon() {
    return null;
  }

  public void uninstallChooserPanel(JColorChooser enclosingChooser) {
    super.uninstallChooserPanel(enclosingChooser);
    removeAll();
  }

  /**
   * Builds and configures the widgets for the GTKColorChooserPanel.
   */
  protected void buildChooser() {
    triangle = new ColorTriangle();
    triangle.setName("GTKColorChooserPanel.triangle");

    // PENDING: when we straighten out user setting opacity, this should
    // be changed.
    label = new OpaqueLabel();
    label.setName("GTKColorChooserPanel.colorWell");
    label.setOpaque(true);
    label.setMinimumSize(new Dimension(67, 32));
    label.setPreferredSize(new Dimension(67, 32));
    label.setMaximumSize(new Dimension(67, 32));

    // PENDING: when we straighten out user setting opacity, this should
    // be changed.
    lastLabel = new OpaqueLabel();
    lastLabel.setName("GTKColorChooserPanel.lastColorWell");
    lastLabel.setOpaque(true);
    lastLabel.setMinimumSize(new Dimension(67, 32));
    lastLabel.setPreferredSize(new Dimension(67, 32));
    lastLabel.setMaximumSize(new Dimension(67, 32));

    hueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 360, 1));
    configureSpinner(hueSpinner, "GTKColorChooserPanel.hueSpinner");
    saturationSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
    configureSpinner(saturationSpinner,
        "GTKColorChooserPanel.saturationSpinner");
    valueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
    configureSpinner(valueSpinner, "GTKColorChooserPanel.valueSpinner");
    redSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
    configureSpinner(redSpinner, "GTKColorChooserPanel.redSpinner");
    greenSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
    configureSpinner(greenSpinner, "GTKColorChooserPanel.greenSpinner");
    blueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
    configureSpinner(blueSpinner, "GTKColorChooserPanel.blueSpinner");

    colorNameTF = new JTextField(8);

    setLayout(new GridBagLayout());

    add(this, "GTKColorChooserPanel.hue", hueSpinner, -1, -1);
    add(this, "GTKColorChooserPanel.red", redSpinner, -1, -1);
    add(this, "GTKColorChooserPanel.saturation", saturationSpinner, -1, -1);
    add(this, "GTKColorChooserPanel.green", greenSpinner, -1, -1);
    add(this, "GTKColorChooserPanel.value", valueSpinner, -1, -1);
    add(this, "GTKColorChooserPanel.blue", blueSpinner, -1, -1);

    add(new JSeparator(SwingConstants.HORIZONTAL), new
        GridBagConstraints(1, 3, 4, 1, 1, 0,
        GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
        new Insets(14, 0, 0, 0), 0, 0));

    add(this, "GTKColorChooserPanel.colorName", colorNameTF, 0, 4);

    add(triangle, new GridBagConstraints(0, 0, 1, 5, 0, 0,
        GridBagConstraints.LINE_START, GridBagConstraints.NONE,
        new Insets(14, 20, 2, 9), 0, 0));

    Box hBox = Box.createHorizontalBox();
    hBox.add(lastLabel);
    hBox.add(label);
    add(hBox, new GridBagConstraints(0, 5, 1, 1, 0, 0,
        GridBagConstraints.CENTER, GridBagConstraints.NONE,
        new Insets(0, 0, 0, 0), 0, 0));

    add(new JSeparator(SwingConstants.HORIZONTAL), new
        GridBagConstraints(0, 6, 5, 1, 1, 0,
        GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
        new Insets(12, 0, 0, 0), 0, 0));
  }

  /**
   * Configures the spinner.
   */
  private void configureSpinner(JSpinner spinner, String name) {
    spinner.addChangeListener(this);
    spinner.setName(name);
    JComponent editor = spinner.getEditor();
    if (editor instanceof JSpinner.DefaultEditor) {
      JFormattedTextField ftf = ((JSpinner.DefaultEditor) editor).
          getTextField();

      ftf.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
    }
  }

  /**
   * Adds the widget creating a JLabel with the specified name.
   */
  private void add(Container parent, String key, JComponent widget,
      int x, int y) {
    JLabel label = new JLabel(UIManager.getString(key + "Text",
        getLocale()));
    String mnemonic = (String) UIManager.get(key + "Mnemonic", getLocale());

    if (mnemonic != null) {
      try {
        label.setDisplayedMnemonic(Integer.parseInt(mnemonic));
      } catch (NumberFormatException nfe) {
      }
      String mnemonicIndex = (String) UIManager.get(key + "MnemonicIndex",
          getLocale());

      if (mnemonicIndex != null) {
        try {
          label.setDisplayedMnemonicIndex(Integer.parseInt(
              mnemonicIndex));
        } catch (NumberFormatException nfe) {
        }
      }
    }
    label.setLabelFor(widget);
    if (x < 0) {
      x = parent.getComponentCount() % 4;
    }
    if (y < 0) {
      y = parent.getComponentCount() / 4;
    }
    GridBagConstraints con = new GridBagConstraints(x + 1, y, 1, 1, 0, 0,
        GridBagConstraints.FIRST_LINE_END, GridBagConstraints.NONE,
        new Insets(4, 0, 0, 4), 0, 0);
    if (y == 0) {
      con.insets.top = 14;
    }
    parent.add(label, con);
    con.gridx++;
    parent.add(widget, con);
  }

  /**
   * Refreshes the display from the model.
   */
  public void updateChooser() {
    if (!settingColor) {
      lastLabel.setBackground(getColorFromModel());
      setColor(getColorFromModel(), true, true, false);
    }
  }

  /**
   * Resets the red component of the selected color.
   */
  private void setRed(int red) {
    setRGB(red << 16 | getColor().getGreen() << 8 | getColor().getBlue());
  }

  /**
   * Resets the green component of the selected color.
   */
  private void setGreen(int green) {
    setRGB(getColor().getRed() << 16 | green << 8 | getColor().getBlue());
  }

  /**
   * Resets the blue component of the selected color.
   */
  private void setBlue(int blue) {
    setRGB(getColor().getRed() << 16 | getColor().getGreen() << 8 | blue);
  }

  /**
   * Sets the hue of the selected color and updates the display if
   * necessary.
   */
  private void setHue(float hue, boolean update) {
    setHSB(hue, saturation, brightness);
    if (update) {
      settingColor = true;
      hueSpinner.setValue(Integer.valueOf((int) (hue * 360)));
      settingColor = false;
    }
  }

  /**
   * Returns the current amount of hue.
   */
  private float getHue() {
    return hue;
  }

  /**
   * Resets the saturation.
   */
  private void setSaturation(float saturation) {
    setHSB(hue, saturation, brightness);
  }

  /**
   * Returns the saturation.
   */
  private float getSaturation() {
    return saturation;
  }

  /**
   * Sets the brightness.
   */
  private void setBrightness(float brightness) {
    setHSB(hue, saturation, brightness);
  }

  /**
   * Returns the brightness.
   */
  private float getBrightness() {
    return brightness;
  }

  /**
   * Sets the saturation and brightness and updates the display if
   * necessary.
   */
  private void setSaturationAndBrightness(float s, float b, boolean update) {
    setHSB(hue, s, b);
    if (update) {
      settingColor = true;
      saturationSpinner.setValue(Integer.valueOf((int) (s * 255)));
      valueSpinner.setValue(Integer.valueOf((int) (b * 255)));
      settingColor = false;
    }
  }

  /**
   * Resets the rgb values.
   */
  private void setRGB(int rgb) {
    Color color = new Color(rgb);

    setColor(color, false, true, true);

    settingColor = true;
    hueSpinner.setValue(Integer.valueOf((int) (hue * 360)));
    saturationSpinner.setValue(Integer.valueOf((int) (saturation * 255)));
    valueSpinner.setValue(Integer.valueOf((int) (brightness * 255)));
    settingColor = false;
  }

  /**
   * Resets the hsb values.
   */
  private void setHSB(float h, float s, float b) {
    Color color = Color.getHSBColor(h, s, b);

    this.hue = h;
    this.saturation = s;
    this.brightness = b;
    setColor(color, false, false, true);

    settingColor = true;
    redSpinner.setValue(Integer.valueOf(color.getRed()));
    greenSpinner.setValue(Integer.valueOf(color.getGreen()));
    blueSpinner.setValue(Integer.valueOf(color.getBlue()));
    settingColor = false;
  }


  /**
   * Rests the color.
   *
   * @param color new Color
   * @param updateSpinners whether or not to update the spinners.
   * @param updateHSB if true, the hsb fields are updated based on the new color
   * @param updateModel if true, the model is set.
   */
  private void setColor(Color color, boolean updateSpinners,
      boolean updateHSB, boolean updateModel) {
    if (color == null) {
      color = Color.BLACK;
    }

    settingColor = true;

    if (updateHSB) {
      float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(),
          color.getBlue(), null);
      hue = hsb[0];
      saturation = hsb[1];
      brightness = hsb[2];
    }

    if (updateModel) {
      ColorSelectionModel model = getColorSelectionModel();
      if (model != null) {
        model.setSelectedColor(color);
      }
    }

    triangle.setColor(hue, saturation, brightness);
    label.setBackground(color);
    // Force Integer to pad the string with 0's by adding 0x1000000 and
    // then removing the first character.
    String hexString = Integer.toHexString(
        (color.getRGB() & 0xFFFFFF) | 0x1000000);
    colorNameTF.setText("#" + hexString.substring(1));

    if (updateSpinners) {
      redSpinner.setValue(Integer.valueOf(color.getRed()));
      greenSpinner.setValue(Integer.valueOf(color.getGreen()));
      blueSpinner.setValue(Integer.valueOf(color.getBlue()));

      hueSpinner.setValue(Integer.valueOf((int) (hue * 360)));
      saturationSpinner.setValue(Integer.valueOf((int) (saturation * 255)));
      valueSpinner.setValue(Integer.valueOf((int) (brightness * 255)));
    }
    settingColor = false;
  }

  public Color getColor() {
    return label.getBackground();
  }

  /**
   * ChangeListener method, updates the necessary display widgets.
   */
  public void stateChanged(ChangeEvent e) {
    if (settingColor) {
      return;
    }
    Color color = getColor();

    if (e.getSource() == hueSpinner) {
      setHue(((Number) hueSpinner.getValue()).floatValue() / 360, false);
    } else if (e.getSource() == saturationSpinner) {
      setSaturation(((Number) saturationSpinner.getValue()).
          floatValue() / 255);
    } else if (e.getSource() == valueSpinner) {
      setBrightness(((Number) valueSpinner.getValue()).
          floatValue() / 255);
    } else if (e.getSource() == redSpinner) {
      setRed(((Number) redSpinner.getValue()).intValue());
    } else if (e.getSource() == greenSpinner) {
      setGreen(((Number) greenSpinner.getValue()).intValue());
    } else if (e.getSource() == blueSpinner) {
      setBlue(((Number) blueSpinner.getValue()).intValue());
    }
  }


  /**
   * Flag indicating the angle, or hue, has changed and the triangle
   * needs to be recreated.
   */
  private static final int FLAGS_CHANGED_ANGLE = 1 << 0;
  /**
   * Indicates the wheel is being dragged.
   */
  private static final int FLAGS_DRAGGING = 1 << 1;
  /**
   * Indicates the triangle is being dragged.
   */
  private static final int FLAGS_DRAGGING_TRIANGLE = 1 << 2;
  /**
   * Indicates a color is being set and we should ignore setColor
   */
  private static final int FLAGS_SETTING_COLOR = 1 << 3;
  /**
   * Indicates the wheel has focus.
   */
  private static final int FLAGS_FOCUSED_WHEEL = 1 << 4;
  /**
   * Indicates the triangle has focus.
   */
  private static final int FLAGS_FOCUSED_TRIANGLE = 1 << 5;


  /**
   * Class responsible for rendering a color wheel and color triangle.
   */
  private class ColorTriangle extends JPanel {

    /**
     * Cached image of the wheel.
     */
    private Image wheelImage;

    /**
     * Cached image of the triangle.
     */
    private Image triangleImage;

    /**
     * Angle triangle is rotated by.
     */
    private double angle;

    /**
     * Boolean bitmask.
     */
    private int flags;

    /**
     * X location of selected color indicator.
     */
    private int circleX;
    /**
     * Y location of selected color indicator.
     */
    private int circleY;


    public ColorTriangle() {
      enableEvents(AWTEvent.FOCUS_EVENT_MASK);
      enableEvents(AWTEvent.MOUSE_EVENT_MASK);
      enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);

      setMinimumSize(new Dimension(getWheelRadius() * 2 + 2,
          getWheelRadius() * 2 + 2));
      setPreferredSize(new Dimension(getWheelRadius() * 2 + 2,
          getWheelRadius() * 2 + 2));

      // We want to handle tab ourself.
      setFocusTraversalKeysEnabled(false);

      // PENDING: this should come from the style.
      getInputMap().put(KeyStroke.getKeyStroke("UP"), "up");
      getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "down");
      getInputMap().put(KeyStroke.getKeyStroke("LEFT"), "left");
      getInputMap().put(KeyStroke.getKeyStroke("RIGHT"), "right");

      getInputMap().put(KeyStroke.getKeyStroke("KP_UP"), "up");
      getInputMap().put(KeyStroke.getKeyStroke("KP_DOWN"), "down");
      getInputMap().put(KeyStroke.getKeyStroke("KP_LEFT"), "left");
      getInputMap().put(KeyStroke.getKeyStroke("KP_RIGHT"), "right");

      getInputMap().put(KeyStroke.getKeyStroke("TAB"), "focusNext");
      getInputMap().put(KeyStroke.getKeyStroke("shift TAB"), "focusLast");

      ActionMap map = (ActionMap) UIManager.get(
          "GTKColorChooserPanel.actionMap");

      if (map == null) {
        map = new ActionMapUIResource();
        map.put("left", new ColorAction("left", 2));
        map.put("right", new ColorAction("right", 3));
        map.put("up", new ColorAction("up", 0));
        map.put("down", new ColorAction("down", 1));
        map.put("focusNext", new ColorAction("focusNext", 4));
        map.put("focusLast", new ColorAction("focusLast", 5));
        UIManager.getLookAndFeelDefaults().put(
            "GTKColorChooserPanel.actionMap", map);
      }
      SwingUtilities.replaceUIActionMap(this, map);
    }

    /**
     * Returns the GTKColorChooserPanel.
     */
    GTKColorChooserPanel getGTKColorChooserPanel() {
      return GTKColorChooserPanel.this;
    }

    /**
     * Gives focus to the wheel.
     */
    void focusWheel() {
      setFocusType(1);
    }

    /**
     * Gives focus to the triangle.
     */
    void focusTriangle() {
      setFocusType(2);
    }

    /**
     * Returns true if the wheel currently has focus.
     */
    boolean isWheelFocused() {
      return isSet(FLAGS_FOCUSED_WHEEL);
    }

    /**
     * Resets the selected color.
     */
    public void setColor(float h, float s, float b) {
      if (isSet(FLAGS_SETTING_COLOR)) {
        return;
      }

      setAngleFromHue(h);
      setSaturationAndBrightness(s, b);
    }

    /**
     * Returns the selected color.
     */
    public Color getColor() {
      return GTKColorChooserPanel.this.getColor();
    }

    /**
     * Returns the x location of the selected color indicator.
     */
    int getColorX() {
      return circleX + getIndicatorSize() / 2 - getWheelXOrigin();
    }

    /**
     * Returns the y location of the selected color indicator.
     */
    int getColorY() {
      return circleY + getIndicatorSize() / 2 - getWheelYOrigin();
    }

    protected void processEvent(AWTEvent e) {
      if (e.getID() == MouseEvent.MOUSE_PRESSED ||
          ((isSet(FLAGS_DRAGGING) || isSet(FLAGS_DRAGGING_TRIANGLE)) &&
              e.getID() == MouseEvent.MOUSE_DRAGGED)) {
        // Assign focus to either the wheel or triangle and attempt
        // to drag either the wheel or triangle.
        int size = getWheelRadius();
        int x = ((MouseEvent) e).getX() - size;
        int y = ((MouseEvent) e).getY() - size;

        if (!hasFocus()) {
          requestFocus();
        }
        if (!isSet(FLAGS_DRAGGING_TRIANGLE) &&
            adjustHue(x, y, e.getID() == MouseEvent.MOUSE_PRESSED)) {
          setFlag(FLAGS_DRAGGING, true);
          setFocusType(1);
        } else if (adjustSB(x, y, e.getID() ==
            MouseEvent.MOUSE_PRESSED)) {
          setFlag(FLAGS_DRAGGING_TRIANGLE, true);
          setFocusType(2);
        } else {
          setFocusType(2);
        }
      } else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
        // Stopped dragging
        setFlag(FLAGS_DRAGGING_TRIANGLE, false);
        setFlag(FLAGS_DRAGGING, false);
      } else if (e.getID() == FocusEvent.FOCUS_LOST) {
        // Reset the flags to indicate no one has focus
        setFocusType(0);
      } else if (e.getID() == FocusEvent.FOCUS_GAINED) {
        // Gained focus, reassign focus to the wheel if no one
        // currently has focus.
        if (!isSet(FLAGS_FOCUSED_TRIANGLE) &&
            !isSet(FLAGS_FOCUSED_WHEEL)) {
          setFlag(FLAGS_FOCUSED_WHEEL, true);
          setFocusType(1);
        }
        repaint();
      }
      super.processEvent(e);
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);

      // Draw the wheel and triangle
      int size = getWheelRadius();
      int width = getWheelWidth();
      Image image = getImage(size);
      g.drawImage(image, getWheelXOrigin() - size,
          getWheelYOrigin() - size, null);

      // Draw the focus indicator for the wheel
      if (hasFocus() && isSet(FLAGS_FOCUSED_WHEEL)) {
        g.setColor(Color.BLACK);
        g.drawOval(getWheelXOrigin() - size, getWheelYOrigin() - size,
            2 * size, 2 * size);
        g.drawOval(getWheelXOrigin() - size + width, getWheelYOrigin() -
            size + width, 2 * (size - width), 2 *
            (size - width));
      }

      // Draw a line on the wheel indicating the selected hue.
      if (Math.toDegrees(Math.PI * 2 - angle) <= 20 ||
          Math.toDegrees(Math.PI * 2 - angle) >= 201) {
        g.setColor(Color.WHITE);
      } else {
        g.setColor(Color.BLACK);
      }
      int lineX0 = (int) (Math.cos(angle) * size);
      int lineY0 = (int) (Math.sin(angle) * size);
      int lineX1 = (int) (Math.cos(angle) * (size - width));
      int lineY1 = (int) (Math.sin(angle) * (size - width));
      g.drawLine(lineX0 + size, lineY0 + size, lineX1 + size,
          lineY1 + size);

      // Draw the focus indicator on the triangle
      if (hasFocus() && isSet(FLAGS_FOCUSED_TRIANGLE)) {
        Graphics g2 = g.create();
        int innerR = getTriangleCircumscribedRadius();
        int a = (int) (3 * innerR / Math.sqrt(3));
        g2.translate(getWheelXOrigin(), getWheelYOrigin());
        ((Graphics2D) g2).rotate(angle + Math.PI / 2);
        g2.setColor(Color.BLACK);
        g2.drawLine(0, -innerR, a / 2, innerR / 2);
        g2.drawLine(a / 2, innerR / 2, -a / 2, innerR / 2);
        g2.drawLine(-a / 2, innerR / 2, 0, -innerR);
        g2.dispose();
      }

      // Draw the selected color indicator.
      g.setColor(Color.BLACK);
      g.drawOval(circleX, circleY, getIndicatorSize() - 1,
          getIndicatorSize() - 1);
      g.setColor(Color.WHITE);
      g.drawOval(circleX + 1, circleY + 1, getIndicatorSize() - 3,
          getIndicatorSize() - 3);
    }

    /**
     * Returns an image representing the triangle and wheel.
     */
    private Image getImage(int size) {
      if (!isSet(FLAGS_CHANGED_ANGLE) && wheelImage != null &&
          wheelImage.getWidth(null) == size * 2) {
        return wheelImage;
      }
      if (wheelImage == null || wheelImage.getWidth(null) != size) {
        wheelImage = getWheelImage(size);
      }
      int innerR = getTriangleCircumscribedRadius();
      int triangleSize = (int) (innerR * 3.0 / 2.0);
      int a = (int) (2 * triangleSize / Math.sqrt(3));
      if (triangleImage == null || triangleImage.getWidth(null) != a) {
        triangleImage = new BufferedImage(a, a,
            BufferedImage.TYPE_INT_ARGB);
      }
      Graphics g = triangleImage.getGraphics();
      g.setColor(new Color(0, 0, 0, 0));
      g.fillRect(0, 0, a, a);
      g.translate(a / 2, 0);
      paintTriangle(g, triangleSize, getColor());
      g.translate(-a / 2, 0);
      g.dispose();

      g = wheelImage.getGraphics();
      g.setColor(new Color(0, 0, 0, 0));
      g.fillOval(getWheelWidth(), getWheelWidth(),
          2 * (size - getWheelWidth()),
          2 * (size - getWheelWidth()));

      double rotate = Math.toRadians(-30.0) + angle;
      g.translate(size, size);
      ((Graphics2D) g).rotate(rotate);
      g.drawImage(triangleImage, -a / 2,
          getWheelWidth() - size, null);
      ((Graphics2D) g).rotate(-rotate);
      g.translate(a / 2, size - getWheelWidth());

      setFlag(FLAGS_CHANGED_ANGLE, false);

      return wheelImage;
    }

    private void paintTriangle(Graphics g, int size, Color color) {
      float[] colors = Color.RGBtoHSB(color.getRed(),
          color.getGreen(),
          color.getBlue(), null);
      float hue = colors[0];
      double dSize = (double) size;
      for (int y = 0; y < size; y++) {
        int maxX = (int) (y * Math.tan(Math.toRadians(30.0)));
        float factor = maxX * 2;
        if (maxX > 0) {
          float value = (float) (y / dSize);
          for (int x = -maxX; x <= maxX; x++) {
            float saturation = (float) x / factor + .5f;
            g.setColor(Color.getHSBColor(hue, saturation, value));
            g.fillRect(x, y, 1, 1);
          }
        } else {
          g.setColor(color);
          g.fillRect(0, y, 1, 1);
        }
      }
    }

    /**
     * Returns a color wheel image for the specified size.
     *
     * @param size Integer giving size of color wheel.
     * @return Color wheel image
     */
    private Image getWheelImage(int size) {
      int minSize = size - getWheelWidth();
      int doubleSize = size * 2;
      BufferedImage image = new BufferedImage(doubleSize, doubleSize,
          BufferedImage.TYPE_INT_ARGB);

      for (int y = -size; y < size; y++) {
        int ySquared = y * y;
        for (int x = -size; x < size; x++) {
          double rad = Math.sqrt(ySquared + x * x);

          if (rad < size && rad > minSize) {
            int rgb = colorWheelLocationToRGB(x, y, rad) |
                0xFF000000;
            image.setRGB(x + size, y + size, rgb);
          }
        }
      }
      wheelImage = image;
      return wheelImage;
    }

    /**
     * Adjusts the saturation and brightness. <code>x</code> and
     * <code>y</code> give the location to adjust to and are relative
     * to the origin of the wheel/triangle.
     *
     * @param x X coordinate on the triangle to adjust to
     * @param y Y coordinate on the triangle to adjust to
     * @param checkLoc if true the location is checked to make sure it is contained in the triangle,
     * if false the location is constrained to fit in the triangle.
     * @return true if the location is valid
     */
    boolean adjustSB(int x, int y, boolean checkLoc) {
      int innerR = getWheelRadius() - getWheelWidth();
      boolean resetXY = false;
      // Invert the axis.
      y = -y;
      if (checkLoc && (x < -innerR || x > innerR || y < -innerR ||
          y > innerR)) {
        return false;
      }
      // Rotate to origin and and verify x is valid.
      int triangleSize = innerR * 3 / 2;
      double x1 = Math.cos(angle) * x - Math.sin(angle) * y;
      double y1 = Math.sin(angle) * x + Math.cos(angle) * y;
      if (x1 < -(innerR / 2)) {
        if (checkLoc) {
          return false;
        }
        x1 = -innerR / 2;
        resetXY = true;
      } else if ((int) x1 > innerR) {
        if (checkLoc) {
          return false;
        }
        x1 = innerR;
        resetXY = true;
      }
      // Verify y location is valid.
      int maxY = (int) ((triangleSize - x1 - innerR / 2.0) *
          Math.tan(Math.toRadians(30.0)));
      if (y1 <= -maxY) {
        if (checkLoc) {
          return false;
        }
        y1 = -maxY;
        resetXY = true;
      } else if (y1 > maxY) {
        if (checkLoc) {
          return false;
        }
        y1 = maxY;
        resetXY = true;
      }
      // Rotate again to determine value and scale
      double x2 = Math.cos(Math.toRadians(-30.0)) * x1 -
          Math.sin(Math.toRadians(-30.0)) * y1;
      double y2 = Math.sin(Math.toRadians(-30.0)) * x1 +
          Math.cos(Math.toRadians(-30.0)) * y1;
      float value = Math.min(1.0f, (float) ((innerR - y2) /
          (double) triangleSize));
      float maxX = (float) (Math.tan(Math.toRadians(30)) * (innerR - y2));
      float saturation = Math.min(1.0f, (float) (x2 / maxX / 2 + .5));

      setFlag(FLAGS_SETTING_COLOR, true);
      if (resetXY) {
        setSaturationAndBrightness(saturation, value);
      } else {
        setSaturationAndBrightness(saturation, value, x +
            getWheelXOrigin(), getWheelYOrigin() - y);
      }
      GTKColorChooserPanel.this.setSaturationAndBrightness(saturation,
          value, true);
      setFlag(FLAGS_SETTING_COLOR, false);
      return true;
    }

    /**
     * Sets the saturation and brightness.
     */
    private void setSaturationAndBrightness(float s, float b) {
      int innerR = getTriangleCircumscribedRadius();
      int triangleSize = innerR * 3 / 2;
      double x = b * triangleSize;
      double maxY = x * Math.tan(Math.toRadians(30.0));
      double y = 2 * maxY * s - maxY;
      x = x - innerR;
      double x1 = Math.cos(Math.toRadians(-60.0) - angle) *
          x - Math.sin(Math.toRadians(-60.0) - angle) * y;
      double y1 = Math.sin(Math.toRadians(-60.0) - angle) * x +
          Math.cos(Math.toRadians(-60.0) - angle) * y;
      int newCircleX = (int) x1 + getWheelXOrigin();
      int newCircleY = getWheelYOrigin() - (int) y1;

      setSaturationAndBrightness(s, b, newCircleX, newCircleY);
    }


    /**
     * Sets the saturation and brightness.
     */
    private void setSaturationAndBrightness(float s, float b,
        int newCircleX, int newCircleY) {
      newCircleX -= getIndicatorSize() / 2;
      newCircleY -= getIndicatorSize() / 2;

      int minX = Math.min(newCircleX, circleX);
      int minY = Math.min(newCircleY, circleY);

      repaint(minX, minY, Math.max(circleX, newCircleX) - minX +
          getIndicatorSize() + 1, Math.max(circleY, newCircleY) -
          minY + getIndicatorSize() + 1);
      circleX = newCircleX;
      circleY = newCircleY;
    }

    /**
     * Adjusts the hue based on the passed in location.
     *
     * @param x X location to adjust to, relative to the origin of the wheel
     * @param y Y location to adjust to, relative to the origin of the wheel
     * @param check if true the location is checked to make sure it is contained in the wheel, if
     * false the location is constrained to fit in the wheel
     * @return true if the location is valid.
     */
    private boolean adjustHue(int x, int y, boolean check) {
      double rad = Math.sqrt(x * x + y * y);
      int size = getWheelRadius();

      if (!check || (rad >= size - getWheelWidth() && rad < size)) {
        // Map the location to an angle and reset hue
        double angle;
        if (x == 0) {
          if (y > 0) {
            angle = Math.PI / 2.0;
          } else {
            angle = Math.PI + Math.PI / 2.0;
          }
        } else {
          angle = Math.atan((double) y / (double) x);
          if (x < 0) {
            angle += Math.PI;
          } else if (angle < 0) {
            angle += 2 * Math.PI;
          }
        }
        setFlag(FLAGS_SETTING_COLOR, true);
        setHue((float) (1.0 - angle / Math.PI / 2), true);
        setFlag(FLAGS_SETTING_COLOR, false);
        setHueAngle(angle);
        setSaturationAndBrightness(getSaturation(), getBrightness());
        return true;
      }
      return false;
    }

    /**
     * Rotates the triangle to accommodate the passed in hue.
     */
    private void setAngleFromHue(float hue) {
      setHueAngle((1.0 - hue) * Math.PI * 2);
    }

    /**
     * Sets the angle representing the hue.
     */
    private void setHueAngle(double angle) {
      double oldAngle = this.angle;

      this.angle = angle;
      if (angle != oldAngle) {
        setFlag(FLAGS_CHANGED_ANGLE, true);
        repaint();
      }
    }

    /**
     * Returns the size of the color indicator.
     */
    private int getIndicatorSize() {
      return 8;
    }

    /**
     * Returns the circumscribed radius of the triangle.
     */
    private int getTriangleCircumscribedRadius() {
      return 72;
    }

    /**
     * Returns the x origin of the wheel and triangle.
     */
    private int getWheelXOrigin() {
      return 85;
    }

    /**
     * Returns y origin of the wheel and triangle.
     */
    private int getWheelYOrigin() {
      return 85;
    }

    /**
     * Returns the width of the wheel.
     */
    private int getWheelWidth() {
      return 13;
    }

    /**
     * Sets the focus to one of: 0 no one, 1 the wheel or 2 the triangle.
     */
    private void setFocusType(int type) {
      if (type == 0) {
        setFlag(FLAGS_FOCUSED_WHEEL, false);
        setFlag(FLAGS_FOCUSED_TRIANGLE, false);
        repaint();
      } else {
        int toSet = FLAGS_FOCUSED_WHEEL;
        int toUnset = FLAGS_FOCUSED_TRIANGLE;

        if (type == 2) {
          toSet = FLAGS_FOCUSED_TRIANGLE;
          toUnset = FLAGS_FOCUSED_WHEEL;
        }
        if (!isSet(toSet)) {
          setFlag(toSet, true);
          repaint();
          setFlag(toUnset, false);
        }
      }
    }

    /**
     * Returns the radius of the wheel.
     */
    private int getWheelRadius() {
      // As far as I can tell, GTK doesn't allow stretching this
      // widget
      return 85;
    }

    /**
     * Updates the flags bitmask.
     */
    private void setFlag(int flag, boolean value) {
      if (value) {
        flags |= flag;
      } else {
        flags &= ~flag;
      }
    }

    /**
     * Returns true if a particular flag has been set.
     */
    private boolean isSet(int flag) {
      return ((flags & flag) == flag);
    }

    /**
     * Returns the RGB color to use for the specified location. The
     * passed in point must be on the color wheel and be relative to the
     * origin of the color wheel.
     *
     * @param x X location to get color for
     * @param y Y location to get color for
     * @param rad Radius from center of color wheel
     * @return integer with red, green and blue components
     */
    private int colorWheelLocationToRGB(int x, int y, double rad) {
      double angle = Math.acos((double) x / rad);
      int rgb;

      if (angle < PI_3) {
        if (y < 0) {
          // FFFF00 - FF0000
          rgb = 0xFF0000 | Math.min(255,
              (int) (255 * angle / PI_3)) << 8;
        } else {
          // FF0000 - FF00FF
          rgb = 0xFF0000 | Math.min(255,
              (int) (255 * angle / PI_3));
        }
      } else if (angle < 2 * PI_3) {
        angle -= PI_3;
        if (y < 0) {
          // 00FF00 - FFFF00
          rgb = 0x00FF00 | Math.max(0, 255 -
              (int) (255 * angle / PI_3)) << 16;
        } else {
          // FF00FF - 0000FF
          rgb = 0x0000FF | Math.max(0, 255 -
              (int) (255 * angle / PI_3)) << 16;
        }
      } else {
        angle -= 2 * PI_3;
        if (y < 0) {
          // 00FFFF - 00FF00
          rgb = 0x00FF00 | Math.min(255,
              (int) (255 * angle / PI_3));
        } else {
          // 0000FF - 00FFFF
          rgb = 0x0000FF | Math.min(255,
              (int) (255 * angle / PI_3)) << 8;
        }
      }
      return rgb;
    }

    /**
     * Increments the hue.
     */
    void incrementHue(boolean positive) {
      float hue = triangle.getGTKColorChooserPanel().getHue();

      if (positive) {
        hue += 1.0f / 360.0f;
      } else {
        hue -= 1.0f / 360.0f;
      }
      if (hue > 1) {
        hue -= 1;
      } else if (hue < 0) {
        hue += 1;
      }
      getGTKColorChooserPanel().setHue(hue, true);
    }
  }


  /**
   * Action class used for colors.
   */
  private static class ColorAction extends AbstractAction {

    private int type;

    ColorAction(String name, int type) {
      super(name);
      this.type = type;
    }

    public void actionPerformed(ActionEvent e) {
      ColorTriangle triangle = (ColorTriangle) e.getSource();

      if (triangle.isWheelFocused()) {
        float hue = triangle.getGTKColorChooserPanel().getHue();

        switch (type) {
          case 0:
          case 2:
            triangle.incrementHue(true);
            break;
          case 1:
          case 3:
            triangle.incrementHue(false);
            break;
          case 4:
            triangle.focusTriangle();
            break;
          case 5:
            compositeRequestFocus(triangle, false);
            break;
        }
      } else {
        int xDelta = 0;
        int yDelta = 0;

        switch (type) {
          case 0:
            // up
            yDelta--;
            break;
          case 1:
            // down
            yDelta++;
            break;
          case 2:
            // left
            xDelta--;
            break;
          case 3:
            // right
            xDelta++;
            break;
          case 4:
            compositeRequestFocus(triangle, true);
            return;
          case 5:
            triangle.focusWheel();
            return;
        }
        triangle.adjustSB(triangle.getColorX() + xDelta,
            triangle.getColorY() + yDelta, true);
      }
    }
  }


  private class OpaqueLabel extends JLabel {

    public boolean isOpaque() {
      return true;
    }
  }
}
